iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0

在上一篇文章中,我們實作了一個簡單的用戶資料管理系統,並展示了如何結合 Rust 與 React 進行全端開發。然而,對於一個完整的應用來說,通常需要有登入頁面,讓使用者先通過身份驗證,然後進入後台管理頁面進行操作。今天,我們將擴展上一個範例,將管理頁面從 App.js 中獨立出來,並且新增一個簡單的登入頁,示範如何使用 <Route> 進行頁面切換。

一、安裝必要的 React 套件

在進行頁面管理之前,我們需要安裝 react-router-dom,這個套件用於實現 React 應用中的路由管理。打開你的終端機,進入 frontend 前端資料夾,執行以下指令來安裝套件:

npm install react-router-dom

安裝完成後,我們就可以在專案中使用 <Route> 來進行頁面的切換了。

二、將管理頁面搬至新的元件

首先,我們將原本的 App.js 中的用戶資料管理部分抽取出來,並搬到一個新的元件中。我們會將它稱為 UserManagement.js,這將會是用來顯示在後台管理系統中的頁面。

在我們修改之前,原本的專案結構是這樣的:

myproject/
├── frontend/
│   ├── build/
│   ├── node_modules/
│   ├── public/
│   ├── src/
│   │   ├── App.css
│   │   └── App.js
│   ├── .gitignore
│   ├── package-lock.json
│   ├── package.json
│   └── README.md
└── rust-restful-api/
    ├── src/
    └── target/
    └──.gitignore
    └──Cargo.lock
    └──Cargo.toml

其中,App.js 文件中包含了用戶資料管理的相關邏輯與頁面結構。為了更好地組織代碼,我們希望將這些內容從 App.js 中抽取出來,並將用戶資料管理部分移至一個新的元件 UserManagement.js,但是記得要修改原本的 function App()...export default App; 當中的 App,改成 UserManagement,然後App.css 也要改成 UserManagement.css ,實際完整修改程式碼如下:

import { useState, useEffect } from 'react'; // 從 React 中導入 useState 和 useEffect,用來管理狀態與副作用
import axios from 'axios'; // 從 axios 套件導入,用來處理 HTTP 請求
import './UserManagement.css'; // 導入外部的 CSS 檔案,設置樣式

function UserManagement() {
  const [users, setUsers] = useState([]); // 定義一個狀態變數 users,用來儲存用戶列表,初始值為空陣列
  const [newUserName, setNewUserName] = useState(''); // 定義一個狀態變數 newUserName,用來儲存新增用戶的名字,初始值為空字串
  const [updateUserName, setUpdateUserName] = useState(''); // 定義一個狀態變數 updateUserName,用來儲存更新用戶的名字,初始值為空字串
  const [userIdToUpdate, setUserIdToUpdate] = useState(''); // 定義一個狀態變數 userIdToUpdate,用來儲存需要更新的用戶 ID,初始值為空字串

  // 在組件首次渲染後,執行獲取用戶列表的函數
  useEffect(() => {
    fetchUsers(); // 執行獲取用戶的函數
  }, []); // 這裡的空陣列表示只在組件首次載入時執行一次

  // 獲取用戶列表的非同步函數
  const fetchUsers = async () => {
    try {
      const response = await axios.get('/users'); // 向後端發送 GET 請求,獲取用戶資料
      setUsers(response.data); // 將獲取到的用戶資料設定到 users 狀態中
    } catch (error) {
      console.error('Error fetching users:', error); // 如果發生錯誤,顯示錯誤訊息
    }
  };

  // 新增用戶的非同步函數
  const createUser = async () => {
    try {
      if (!newUserName) return; // 如果沒有輸入用戶名稱,則不執行新增操作
      await axios.post('/users', { name: newUserName }); // 向後端發送 POST 請求,傳送新用戶資料
      setNewUserName(''); // 清空輸入框的內容
      fetchUsers(); // 重新獲取用戶列表,更新畫面
    } catch (error) {
      console.error('Error creating user:', error); // 如果發生錯誤,顯示錯誤訊息
    }
  };

  // 刪除用戶的非同步函數
  const deleteUser = async (id) => {
    try {
      await axios.delete(`/users/${id}`); // 向後端發送 DELETE 請求,刪除指定 ID 的用戶
      fetchUsers(); // 重新獲取用戶列表,更新畫面
    } catch (error) {
      console.error('Error deleting user:', error); // 如果發生錯誤,顯示錯誤訊息
    }
  };

  // 更新用戶的非同步函數
  const updateUser = async () => {
    try {
      if (!userIdToUpdate || !updateUserName) return; // 如果沒有輸入要更新的用戶 ID 或新名稱,則不執行更新操作
      await axios.put(`/users/${userIdToUpdate}`, { name: updateUserName }); // 向後端發送 PUT 請求,更新指定 ID 的用戶資料
      setUserIdToUpdate(''); // 清空輸入框的內容
      setUpdateUserName(''); // 清空輸入框的內容
      fetchUsers(); // 重新獲取用戶列表,更新畫面
    } catch (error) {
      console.error('Error updating user:', error); // 如果發生錯誤,顯示錯誤訊息
    }
  };

  return (
    <div className="App"> {/* 最外層的 div,套用 CSS 樣式 App */}
      <header className="App-header"> {/* 頁面的 header,套用 CSS 樣式 App-header */}
        <h1>會員資料管理系統</h1> {/* 頁面標題 */}

        <div className="form-section"> {/* 用於表單的區塊,包含新增和更新會員 */}
          <div className="form-group"> {/* 表單組塊,用於新增會員 */}
            <h2>新增會員</h2> {/* 小標題:新增會員 */}
            <input
              type="text"
              value={newUserName} // 綁定新用戶名稱的狀態變數
              onChange={(e) => setNewUserName(e.target.value)} // 當輸入變更時,更新 newUserName 狀態
              placeholder="輸入會員名稱" // 提示使用者輸入會員名稱
            />
            <button onClick={createUser}>新增會員</button> {/* 點擊按鈕時執行 createUser 函數 */}
          </div>

          <div className="form-group"> {/* 表單組塊,用於更新會員 */}
            <h2>更新會員</h2> {/* 小標題:更新會員 */}
            <input
              type="text"
              value={userIdToUpdate} // 綁定需要更新的會員 ID 的狀態變數
              onChange={(e) => setUserIdToUpdate(e.target.value)} // 當輸入變更時,更新 userIdToUpdate 狀態
              placeholder="輸入會員ID" // 提示使用者輸入會員 ID
            />
            <input
              type="text"
              value={updateUserName} // 綁定新的會員名稱的狀態變數
              onChange={(e) => setUpdateUserName(e.target.value)} // 當輸入變更時,更新 updateUserName 狀態
              placeholder="輸入新的會員名稱" // 提示使用者輸入新的會員名稱
            />
            <button onClick={updateUser}>更新會員</button> {/* 點擊按鈕時執行 updateUser 函數 */}
          </div>
        </div>

        <div className="table-section"> {/* 表格區塊,用於顯示會員列表 */}
          <h2>會員列表</h2> {/* 小標題:會員列表 */}
          <table className="user-table"> {/* 使用者表格,套用 CSS 樣式 user-table */}
            <thead>
              <tr>
                <th>會員ID</th> {/* 表格標題:會員 ID */}
                <th>會員名稱</th> {/* 表格標題:會員名稱 */}
                <th>操作</th> {/* 表格標題:操作 */}
              </tr>
            </thead>
            <tbody>
              {users.map((user) => ( // 遍歷 users 陣列,顯示每個用戶的資料
                <tr key={user.id}> {/* 每一列的唯一 key 為 user.id */}
                  <td>{user.id}</td> {/* 顯示用戶的 ID */}
                  <td>{user.name}</td> {/* 顯示用戶的名稱 */}
                  <td>
                    <button className="edit-btn" onClick={() => deleteUser(user.id)}>刪除</button> {/* 刪除按鈕,點擊後執行 deleteUser 函數 */}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </header>
    </div>
  );
}

export default UserManagement; // 導出 UserManagement 組件,以便在其他地方使用

記得 App.css 也要改成 UserManagement.css 放在 components 資料夾,然後我們還會需要建立一個新的分頁,以 Login.jsLogin.css 命名,搬運後的專案結構會變成這樣:

myproject/
├── frontend/
│   ├── build/
│   ├── node_modules/
│   ├── public/
│   ├── src/
│   │   ├── components/
│   │   │   ├── UserManagement.js  # 用戶資料管理頁面
│   │   │   ├── UserManagement.css # 用戶資料管理頁面樣式
│   │   │   ├── Login.js           # 登入頁面
│   │   │   ├── Login.css          # 登入頁面樣式
│   │   ├── App.js                 # 路由邏輯,移除了管理頁邏輯
│   │   └── App.css
│   └── .gitignore
│   └── package-lock.json
│   └── package.json
│   └── README.md
└── rust-restful-api/
    ├── src/
    └── target/
    └── .gitignore
    └── Cargo.lock
    └── Cargo.toml

我們新增了一個 components 資料夾來存放拆分出來的元件,其中 UserManagement.js 是用來顯示在後台管理系統中的頁面,Login.js 則是新的登入頁面。

這樣的調整讓專案結構更加清晰,並且使得每個元件的責任分離,便於管理和維護。後續我們會在 App.js 中使用路由來管理不同頁面的顯示,現在我們要先完成登入頁的內容設定。

三、建立登入頁面

接下來,我們將建立一個簡單的登入頁面,讓使用者輸入帳號與密碼進行登入,登入成功後跳轉到管理頁面。

建立登入頁

src/components/Login.js

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './Login.css';

function Login() {
  const [username, setUsername] = useState(''); // 帳號
  const [password, setPassword] = useState(''); // 密碼
  const navigate = useNavigate(); // 用於頁面導航

  const handleLogin = (e) => {
    e.preventDefault();
    // 簡單的驗證邏輯(實際上應該連接後端進行認證)
    if (username === 'admin' && password === 'password') {
      navigate('/management'); // 登入成功,跳轉到管理頁面
    } else {
      alert('帳號或密碼錯誤');
    }
  };

  return (
    <div className="login">
      <h2>登入頁面</h2>
      <form onSubmit={handleLogin}>
        <div className="form-group">
          <label htmlFor="username">帳號</label>
          <input
            type="text"
            id="username"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            required
          />
        </div>
        <div className="form-group">
          <label htmlFor="password">密碼</label>
          <input
            type="password"
            id="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
        </div>
        <button type="submit">登入</button>
      </form>
    </div>
  );
}

export default Login;

這個登入頁面會簡單檢查帳號是否為 admin、密碼是否為 password,如果驗證成功,使用者會被導向用戶管理頁面。當然,實際應用中應該與後端進行身份驗證。

設定樣式

我們也可以設置一些簡單的樣式,讓頁面看起來更美觀。這是 Login.css 的範例:

/* 確保 body 和 html 的高度佔滿整個視窗 */

html,
body {
    height: 100%;
    margin: 0;
    background-color: #f9f3e7;
    /* 背景色和登入頁背景一致 */
}


/* 父元素的 display 設定為 flex,以實現垂直和水平居中 */

.login {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    height: 100vh;
    /* 確保區塊佔滿整個視窗高度 */
    padding: 20px;
    background-color: #f9f3e7;
    border-radius: 10px;
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
    width: 100%;
    margin-left: auto;
    margin-right: auto;
}

.form-group {
    margin: 20px 0;
    text-align: left;
}

label {
    display: block;
    margin-bottom: 8px;
    font-size: 14px;
    color: #5f5f5f;
}

input {
    padding: 12px;
    width: 100%;
    margin: 8px 0;
    border: 1px solid #ccc;
    border-radius: 5px;
    box-sizing: border-box;
    background-color: #fcfbf9;
    font-size: 16px;
}

button {
    padding: 12px 24px;
    background-color: #e07a5f;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    width: 100%;
    font-size: 16px;
    margin-top: 20px;
    transition: background-color 0.3s ease;
}

button:hover {
    background-color: #c5533d;
}

Login.css 一樣放在 components 資料夾內讓 Login.js 引用就可以囉

四、設置路由並進行頁面切換

現在,我們來修改 App.js,加入登入頁面和管理頁面的路由邏輯:

src/App.js

import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; // 導入路由相關模組
import Login from './components/Login'; // 登入頁
import UserManagement from './components/UserManagement'; // 用戶管理頁
import './App.css';

function App() {
  return (
    <Router>
      <div className="App">
        {/* 設置路由 */}
        <Routes>
          <Route path="/" element={<Login />} /> {/* 預設路徑為登入頁 */}
          <Route path="/management" element={<UserManagement />} /> {/* 管理頁 */}
        </Routes>
      </div>
    </Router>
  );
}

export default App;

在這裡,我們設置了兩個主要的路由:

  • /:對應登入頁面。
  • /management:對應用戶管理頁面,使用者登入成功後會被導向此頁。

五、重新打包並測試

現在,整個應用架構已經完成。我們只需要再次執行 npm run build 來打包 React 前端應用,並啟動 Rust 後端伺服器即可測試登入與分頁功能。

# frontend 路徑下執行,以重新打包
npm run build

# rust-restful-api 路徑下執行,開啟網頁應用
cargo run

當你進入 http://127.0.0.1:8000,你將首先看到登入頁面,輸入正確的帳號和密碼後,會跳轉到會員管理的後台頁面。

登入頁面
https://ithelp.ithome.com.tw/upload/images/20241011/20121176dKEfVjIIt6.png

輸入帳號為 admin 密碼為 password 之後,就會進入到上一篇的會員資料管理頁

https://ithelp.ithome.com.tw/upload/images/20241011/20121176iRWxlQRTRk.png

這就是透過 <Route> 元件進行 React 網頁開發應用的分頁管理工具的範例,接下來我們在詳細說明一下上面的程式碼的細節。

六、程式碼詳解

在這個段落中,我們將逐步解析關鍵的程式碼部分,並結合 React 元件化設計React Hooks 的概念,幫助讀者深入理解每個部分的運作原理與背後的邏輯。

1. React 元件化設計的概念

React 的元件化設計是一個非常核心的思想,它將 UI 切割成許多可重複使用的元件。每個元件負責一小部分的功能或視覺展示,並能獨立運作。這樣的結構讓代碼變得更加模組化和清晰,元件可以很容易被重複使用或單獨進行測試。

在我們的專案中,Login.jsUserManagement.js 都是功能性元件 (Functional Components),它們通過接收輸入 (props) 來決定輸出的渲染結果。在 React 16.8 版本之後,React 引入了 Hooks,使得在功能性元件中使用狀態與其他 React 特性成為可能,而無需使用類別元件。

2. React Hooks 的機制

Hooks 是 React 的一個關鍵功能,允許你在不需要類別元件的情況下使用狀態、生命週期等特性。這裡我們用到了兩個最常見的 Hooks——useStateuseEffect

  • useState:用於在功能性元件中添加狀態。每次狀態改變時,React 會重新渲染元件以反映新的狀態。

    例如,在 UserManagementLogin 中,我們使用 useState 來管理輸入框的數值和用戶列表:

    const [users, setUsers] = useState([]); // 管理用戶列表
    const [newUserName, setNewUserName] = useState(''); // 管理新用戶名
    
  • useEffect:用於執行副作用操作,例如數據請求、訂閱或手動操作 DOM。useEffect 是一個具有副作用的 Hook,它會在組件渲染完成後運行。

    UserManagement 中,我們用 useEffect 來在組件掛載後立即發起數據請求:

    useEffect(() => {
      fetchUsers(); // 獲取用戶數據
    }, []); // 空陣列代表只在第一次渲染時執行一次
    

    useEffect 會根據它的依賴項(這裡是空陣列 [])來決定何時執行。這樣的機制讓我們能夠控制元件的生命週期行為,並在需要的時候執行特定操作。

3. react-router-dom 的應用

react-router-dom 是 React 的路由管理工具,它允許我們在單頁應用中創建多個不同的頁面,並在這些頁面之間進行無縫切換。這讓應用變得更加動態和互動性更強。在這裡,我們利用以下核心組件來實現頁面之間的切換:

  • <Router>:它是路由管理的容器,所有路由設定都應包含在其中。
  • <Routes><Route>:這些用來定義不同路徑與對應的元件顯示邏輯。

App.js 中,我們定義了兩個路由路徑:

import Login from './components/Login'; // 登入頁
import UserManagement from './components/UserManagement'; // 用戶管理頁

...

<Routes>
  <Route path="/" element={<Login />} /> {/* 預設路徑為登入頁 */}
  <Route path="/management" element={<UserManagement />} /> {/* 管理頁 */}
</Routes>

"/" 對應 Login 登入頁面,"/management" 對應 UserManagement 用戶管理頁面。當使用者輸入正確的帳號密碼時,頁面會跳轉至管理頁。其中 LoginUserManagement 都是在該程式碼內已經被編輯好的 元件,並且透過 export default Login;export default UserManagement; 的方式宣告可被引用,因此在 App.js 當中就可以透過 import 方式加入到 <Route> 內當作完整分頁。

4. 登入頁面的邏輯

登入頁面 Login.js 的邏輯非常簡單,使用了 useState 來處理帳號和密碼的輸入,並利用 useNavigate 進行頁面的跳轉。

const [username, setUsername] = useState(''); // 帳號
const [password, setPassword] = useState(''); // 密碼
const navigate = useNavigate(); // 用於頁面導航

當使用者點擊登入按鈕時,會觸發 handleLogin 函數。此函數會檢查輸入的帳號和密碼是否正確,並根據結果決定是否跳轉到用戶管理頁面:

if (username === 'admin' && password === 'password') {
  navigate('/management'); // 登入成功,跳轉到管理頁面
} else {
  alert('帳號或密碼錯誤');
}

這裡的頁面跳轉是透過 useNavigate 來實現的,它是 React 路由提供的工具,用於程式性地進行頁面跳轉。

5. 用戶管理頁面的邏輯

用戶管理頁面 UserManagement.js 使用了 axios 來進行 HTTP 請求,並使用 useState 來管理用戶資料。

獲取用戶列表

當頁面加載時,useEffect 會自動調用 fetchUsers 函數,這個函數向後端發送一個 GET 請求來獲取所有的用戶資料,並將它們存儲在 users 狀態變數中。

useEffect(() => {
  fetchUsers();
}, []); // 空依賴陣列表示只在頁面加載時執行一次

const fetchUsers = async () => {
  try {
    const response = await axios.get('/users');
    setUsers(response.data); // 將獲取到的用戶資料存入狀態
  } catch (error) {
    console.error('Error fetching users:', error);
  }
};

新增用戶

當使用者輸入新用戶名並點擊新增按鈕時,createUser 函數會被觸發。這個函數會向後端發送一個 POST 請求,將新的用戶資料提交到伺服器,並在成功後更新用戶列表:

const createUser = async () => {
  try {
    if (!newUserName) return; // 如果沒有輸入用戶名,則不執行操作
    await axios.post('/users', { name: newUserName });
    setNewUserName(''); // 清空輸入框
    fetchUsers(); // 重新獲取用戶列表
  } catch (error) {
    console.error('Error creating user:', error);
  }
};

刪除與更新用戶

這些邏輯與新增用戶相似,都是通過發送 HTTP 請求來實現的,具體分別使用 DELETEPUT 請求來刪除或更新用戶。

  • 刪除用戶
const deleteUser = async (id) => {
  try {
    await axios.delete(`/users/${id}`);
    fetchUsers(); // 刪除後重新獲取用戶列表
  } catch (error) {
    console.error('Error deleting user:', error);
  }
};
  • 更新用戶
const updateUser = async () => {
  try {
    if (!userIdToUpdate || !updateUserName) return; // 如果未輸入用戶 ID 或新名稱,則不執行操作
    await axios.put(`/users/${userIdToUpdate}`, { name: updateUserName });
    setUserIdToUpdate('');
    setUpdateUserName('');
    fetchUsers(); // 更新後重新獲取用戶列表
  } catch (error) {
    console.error('Error updating user:', error);
  }
};

這些程式碼展示了如何通過 React 的元件化設計來進行頁面開發,以及如何使用 Hooks 來管理狀態和生命週期事件。React Hooks 不僅讓功能性元件變得更直觀,也使得代碼更加簡潔明瞭,讓我們能夠清晰地處理不同的頁面邏輯。

此外,我們也展示了如何通過 react-router-dom 來實現多頁面導航,使得應用能夠在單頁應用 (SPA) 的架構中實現類似傳統多頁應用的頁面切換效果。

總結

這篇我們展示了如何延伸上一個專案範例,展示了 React 如何進行多分頁管理,我們加入了簡單的登入頁來模擬登入後切換分頁的功能,可以看得出來我們通過使用 <Route> 可以輕鬆實現各分頁的路由管理,這樣一來每個分頁就可以用獨立的 React 元件來進行開發,這顯示了在前端開發上的彈性,當然,如果想要限制更嚴格的登入權限,就還需要加入一些登入狀態的判斷機制了。

今天的主題內容都是 React 的前端開發技術面,是不是代表 Rust 的部分都已經講完可以下課了呢?當然沒有,所以下一篇,我們要再拉回來到 Rust 了,不過我們的系列文章已經到了尾聲,下一篇是時候該紮穩馬步了,來個總複習與能力測驗了!


上一篇
[Day 27] Rust 的 Web 應用(四):Rust + React 全端開發
下一篇
[Day 29] Rust 學海無涯:紮穩馬步-總複習
系列文
從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言